@susonwaiba/react-media-uploader 0.1.2 → 0.1.4
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/.github/workflows/release.yml +25 -0
- package/NEXTJS_INTEGRATION.md +128 -128
- package/README.md +118 -106
- package/bun.lock +37 -0
- package/dist/hooks/use-media-uploader.d.ts +8 -1
- package/dist/hooks/use-media-uploader.js +74 -32
- package/package.json +42 -40
- package/src/hooks/use-media-uploader.ts +315 -261
- package/src/index.ts +3 -3
- package/src/lib/media-helper.ts +153 -153
- package/src/types/media.ts +47 -47
- package/tsconfig.json +19 -19
|
@@ -1,261 +1,315 @@
|
|
|
1
|
-
import {
|
|
2
|
-
markMediaAsCanceled,
|
|
3
|
-
generateFileHash,
|
|
4
|
-
generateUploadUrl,
|
|
5
|
-
uploadToStorage,
|
|
6
|
-
markMediaAsTemp,
|
|
7
|
-
markMediaAsActive,
|
|
8
|
-
generateMediaType,
|
|
9
|
-
} from "@/lib/media-helper";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
media:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
1
|
+
import {
|
|
2
|
+
markMediaAsCanceled,
|
|
3
|
+
generateFileHash,
|
|
4
|
+
generateUploadUrl,
|
|
5
|
+
uploadToStorage,
|
|
6
|
+
markMediaAsTemp,
|
|
7
|
+
markMediaAsActive,
|
|
8
|
+
generateMediaType,
|
|
9
|
+
} from "@/lib/media-helper";
|
|
10
|
+
import {
|
|
11
|
+
type Media,
|
|
12
|
+
MediaStatusEnum,
|
|
13
|
+
type MediaItem,
|
|
14
|
+
MediaTypeEnum,
|
|
15
|
+
} from "@/types/media";
|
|
16
|
+
import { type AxiosProgressEvent } from "axios";
|
|
17
|
+
import { useState } from "react";
|
|
18
|
+
|
|
19
|
+
export interface UploadMediaInfo extends Omit<AxiosProgressEvent, "event"> {
|
|
20
|
+
event?: undefined;
|
|
21
|
+
cancel?: () => Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface UseMediaUploaderProps<T extends object> {
|
|
25
|
+
defaultValues?: T;
|
|
26
|
+
mediaUploadSuccessStatus?: MediaStatusEnum;
|
|
27
|
+
enableManualUpload?: boolean;
|
|
28
|
+
serverConfig?: {
|
|
29
|
+
additionalHeaders?: Record<string, string>;
|
|
30
|
+
generateUploadUrl?: string;
|
|
31
|
+
onGenerateUploadUrl?: (
|
|
32
|
+
media: Partial<Media>,
|
|
33
|
+
) => Promise<{ item: Media; uploadUrl: string }>;
|
|
34
|
+
markMediaAsTemp?: string;
|
|
35
|
+
onMarkMediaAsTemp?: (
|
|
36
|
+
mediaIds: (string | number)[],
|
|
37
|
+
) => Promise<Partial<Media>[]>;
|
|
38
|
+
markMediaAsActive?: string;
|
|
39
|
+
onMarkMediaAsActive?: (
|
|
40
|
+
mediaIds: (string | number)[],
|
|
41
|
+
) => Promise<Partial<Media>[]>;
|
|
42
|
+
markMediaAsCanceled?: string;
|
|
43
|
+
onMarkMediaAsCanceled?: (
|
|
44
|
+
mediaIds: (string | number)[],
|
|
45
|
+
) => Promise<Partial<Media>[]>;
|
|
46
|
+
};
|
|
47
|
+
onUploadSuccess?: (currentValues: any) => Promise<void>;
|
|
48
|
+
onUploadFailure?: (uploadRes: any) => Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface UseMediaUploaderResponse<T extends object> {
|
|
52
|
+
values: T;
|
|
53
|
+
setValues: (val: T) => void;
|
|
54
|
+
enableManualUpload?: boolean;
|
|
55
|
+
uploadManually: () => Promise<T>;
|
|
56
|
+
mediaItems: Record<string, MediaItem>;
|
|
57
|
+
setMediaItems: (items: Record<string, MediaItem>) => void;
|
|
58
|
+
uploadInfos: Record<string, UploadMediaInfo>;
|
|
59
|
+
onFileInputChange: (e: React.ChangeEvent<HTMLInputElement>) => Promise<void>;
|
|
60
|
+
onFileChange: (file: File, name: string, multiple?: boolean) => Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function useMediaUploader<T extends object>({
|
|
64
|
+
defaultValues,
|
|
65
|
+
mediaUploadSuccessStatus = MediaStatusEnum.TEMP,
|
|
66
|
+
enableManualUpload = false,
|
|
67
|
+
serverConfig,
|
|
68
|
+
onUploadSuccess,
|
|
69
|
+
onUploadFailure,
|
|
70
|
+
}: UseMediaUploaderProps<T> = {}): UseMediaUploaderResponse<T> {
|
|
71
|
+
const [values, setValues] = useState<T>(defaultValues ?? ({} as T));
|
|
72
|
+
const [mediaItems, setMediaItems] = useState<Record<string, MediaItem>>({});
|
|
73
|
+
const [uploadInfos, setUploadInfos] = useState<
|
|
74
|
+
Record<string, UploadMediaInfo>
|
|
75
|
+
>({});
|
|
76
|
+
|
|
77
|
+
const onFileInputChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
78
|
+
const target = e.target as any;
|
|
79
|
+
const name = target.name;
|
|
80
|
+
const multiple = target.multiple || false;
|
|
81
|
+
if (target.files && target.files.length) {
|
|
82
|
+
for (const file of target.files) {
|
|
83
|
+
onFileChange(file, name, multiple);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const onFileChange = async (
|
|
89
|
+
file: File,
|
|
90
|
+
name: string,
|
|
91
|
+
multiple: boolean = false,
|
|
92
|
+
) => {
|
|
93
|
+
const localId = crypto.randomUUID();
|
|
94
|
+
const mediaType = await generateMediaType(file);
|
|
95
|
+
let tempPreviewUrl: string | undefined = undefined;
|
|
96
|
+
if (mediaType === MediaTypeEnum.IMAGE) {
|
|
97
|
+
tempPreviewUrl = URL.createObjectURL(file);
|
|
98
|
+
}
|
|
99
|
+
const item: MediaItem = {
|
|
100
|
+
localId,
|
|
101
|
+
name,
|
|
102
|
+
multiple,
|
|
103
|
+
file,
|
|
104
|
+
tempPreviewUrl,
|
|
105
|
+
media: {
|
|
106
|
+
type: mediaType,
|
|
107
|
+
name: file.name,
|
|
108
|
+
mimeType: file.type,
|
|
109
|
+
size: file.size,
|
|
110
|
+
checksum: await generateFileHash(file),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
setMediaItems((previous) => {
|
|
114
|
+
const newState = { ...previous };
|
|
115
|
+
newState[localId] = item;
|
|
116
|
+
return newState;
|
|
117
|
+
});
|
|
118
|
+
setValues((previous) => {
|
|
119
|
+
const newState = { ...previous };
|
|
120
|
+
newState[name] = tempPreviewUrl;
|
|
121
|
+
return newState;
|
|
122
|
+
});
|
|
123
|
+
if (!enableManualUpload) {
|
|
124
|
+
await uploadMediaFile(item);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const uploadMediaFile = async (item: MediaItem): Promise<T | undefined> => {
|
|
129
|
+
let uploadData:
|
|
130
|
+
| {
|
|
131
|
+
item: Media;
|
|
132
|
+
uploadUrl: string;
|
|
133
|
+
}
|
|
134
|
+
| undefined;
|
|
135
|
+
if (serverConfig?.onGenerateUploadUrl) {
|
|
136
|
+
uploadData = await serverConfig?.onGenerateUploadUrl(item?.media);
|
|
137
|
+
} else {
|
|
138
|
+
const uploadUrlRes = await generateUploadUrl({
|
|
139
|
+
url: serverConfig?.generateUploadUrl,
|
|
140
|
+
additionalHeaders: serverConfig?.additionalHeaders,
|
|
141
|
+
media: item?.media,
|
|
142
|
+
});
|
|
143
|
+
uploadData = uploadUrlRes.data;
|
|
144
|
+
}
|
|
145
|
+
if (uploadData?.item && uploadData?.uploadUrl) {
|
|
146
|
+
item["media"] = uploadData?.item;
|
|
147
|
+
setMediaItems((previous) => {
|
|
148
|
+
const newState = { ...previous };
|
|
149
|
+
newState[item.localId] = item;
|
|
150
|
+
return newState;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const abortController = new AbortController();
|
|
154
|
+
const uploadRes = await uploadToStorage({
|
|
155
|
+
uploadUrl: uploadData?.uploadUrl,
|
|
156
|
+
file: item.file,
|
|
157
|
+
onUploadProgress: (progressEvent) => {
|
|
158
|
+
const currentUploadInfo = {
|
|
159
|
+
...progressEvent,
|
|
160
|
+
event: undefined,
|
|
161
|
+
cancel: async () => {
|
|
162
|
+
abortController.abort();
|
|
163
|
+
setUploadInfos((previous) => {
|
|
164
|
+
const newState = { ...previous };
|
|
165
|
+
if (newState[item.localId]) {
|
|
166
|
+
delete newState[item.localId];
|
|
167
|
+
}
|
|
168
|
+
return newState;
|
|
169
|
+
});
|
|
170
|
+
if (serverConfig?.markMediaAsCanceled) {
|
|
171
|
+
await markMediaAsCanceled({
|
|
172
|
+
url: serverConfig?.markMediaAsCanceled,
|
|
173
|
+
additionalHeaders: serverConfig?.additionalHeaders,
|
|
174
|
+
mediaIds: [uploadData?.item?.id],
|
|
175
|
+
});
|
|
176
|
+
} else if (serverConfig?.onMarkMediaAsCanceled) {
|
|
177
|
+
await serverConfig.onMarkMediaAsCanceled([
|
|
178
|
+
uploadData?.item?.id,
|
|
179
|
+
]);
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
setUploadInfos((previous) => {
|
|
184
|
+
const newState = { ...previous };
|
|
185
|
+
newState[item.localId] = currentUploadInfo;
|
|
186
|
+
return newState;
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
abortController,
|
|
190
|
+
});
|
|
191
|
+
if (uploadRes?.status === 201 || uploadRes?.status === 200) {
|
|
192
|
+
return await onMediaUploadSuccess(item);
|
|
193
|
+
} else if (onUploadFailure) {
|
|
194
|
+
await onUploadFailure(uploadRes);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const onMediaUploadSuccess = async (
|
|
201
|
+
item: MediaItem,
|
|
202
|
+
): Promise<T | undefined> => {
|
|
203
|
+
if (item.media.id) {
|
|
204
|
+
let markResItems: Partial<Media>[] = [];
|
|
205
|
+
if (mediaUploadSuccessStatus === MediaStatusEnum.TEMP) {
|
|
206
|
+
if (serverConfig?.onMarkMediaAsTemp) {
|
|
207
|
+
markResItems = await serverConfig.onMarkMediaAsTemp([item.media.id]);
|
|
208
|
+
} else {
|
|
209
|
+
const res = await markMediaAsTemp({
|
|
210
|
+
url: serverConfig?.markMediaAsTemp,
|
|
211
|
+
additionalHeaders: serverConfig?.additionalHeaders,
|
|
212
|
+
mediaIds: [item.media.id],
|
|
213
|
+
});
|
|
214
|
+
if (res?.data?.items?.length) {
|
|
215
|
+
markResItems = res.data.items;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} else if (mediaUploadSuccessStatus === MediaStatusEnum.ACTIVE) {
|
|
219
|
+
if (serverConfig.onMarkMediaAsActive) {
|
|
220
|
+
markResItems = await serverConfig.onMarkMediaAsActive([
|
|
221
|
+
item.media.id,
|
|
222
|
+
]);
|
|
223
|
+
} else {
|
|
224
|
+
const res = await markMediaAsActive({
|
|
225
|
+
url: serverConfig?.markMediaAsActive,
|
|
226
|
+
additionalHeaders: serverConfig?.additionalHeaders,
|
|
227
|
+
mediaIds: [item.media.id],
|
|
228
|
+
});
|
|
229
|
+
if (res?.data?.items?.length) {
|
|
230
|
+
markResItems = res.data.items;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (markResItems?.length) {
|
|
235
|
+
const newMedia = markResItems[0];
|
|
236
|
+
|
|
237
|
+
item["media"] = newMedia;
|
|
238
|
+
setMediaItems((previous) => {
|
|
239
|
+
const newState = { ...previous };
|
|
240
|
+
newState[item.localId] = item;
|
|
241
|
+
return newState;
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const currentValues: any = {};
|
|
245
|
+
if (item.multiple) {
|
|
246
|
+
currentValues[item.name] = [newMedia.id];
|
|
247
|
+
} else {
|
|
248
|
+
currentValues[item.name] = newMedia.id;
|
|
249
|
+
}
|
|
250
|
+
setValues((previous: T) => {
|
|
251
|
+
const newState: any = { ...previous };
|
|
252
|
+
if (item.multiple) {
|
|
253
|
+
if (Array.isArray(newState[item.name])) {
|
|
254
|
+
newState[item.name].push(currentValues[item.name]);
|
|
255
|
+
} else {
|
|
256
|
+
newState[item.name] = [currentValues[item.name]];
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
newState[item.name] = currentValues[item.name];
|
|
260
|
+
}
|
|
261
|
+
return newState;
|
|
262
|
+
});
|
|
263
|
+
if (onUploadSuccess) {
|
|
264
|
+
await onUploadSuccess(currentValues);
|
|
265
|
+
}
|
|
266
|
+
return currentValues;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return undefined;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const uploadManually = async () => {
|
|
273
|
+
const uploadInfoIds = Object.keys(uploadInfos);
|
|
274
|
+
const mediaItemsToBeUploaded = Object.values(mediaItems).filter(
|
|
275
|
+
(item) => !uploadInfoIds?.includes(item.localId),
|
|
276
|
+
);
|
|
277
|
+
const uploadResponses = await Promise.all(
|
|
278
|
+
mediaItemsToBeUploaded?.map(async (item) => await uploadMediaFile(item)),
|
|
279
|
+
);
|
|
280
|
+
const result: any = { ...values };
|
|
281
|
+
for (const uploadResponse of uploadResponses.filter(
|
|
282
|
+
(item) => item !== undefined,
|
|
283
|
+
)) {
|
|
284
|
+
for (const key in uploadResponse) {
|
|
285
|
+
if (Array.isArray(result[key])) {
|
|
286
|
+
if (
|
|
287
|
+
Array.isArray(uploadResponse[key]) &&
|
|
288
|
+
uploadResponse[key]?.length
|
|
289
|
+
) {
|
|
290
|
+
for (const mediaId of uploadResponse[key]) {
|
|
291
|
+
if (!result[key]?.includes(mediaId)) {
|
|
292
|
+
result[key].push(mediaId);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
} else {
|
|
297
|
+
result[key] = uploadResponse[key];
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return result;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
values,
|
|
306
|
+
setValues,
|
|
307
|
+
enableManualUpload,
|
|
308
|
+
uploadManually,
|
|
309
|
+
mediaItems,
|
|
310
|
+
setMediaItems,
|
|
311
|
+
uploadInfos,
|
|
312
|
+
onFileInputChange,
|
|
313
|
+
onFileChange,
|
|
314
|
+
};
|
|
315
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export * from "@/hooks/use-media-uploader";
|
|
2
|
-
export * from "@/lib/media-helper";
|
|
3
|
-
export * from "@/types/media";
|
|
1
|
+
export * from "@/hooks/use-media-uploader";
|
|
2
|
+
export * from "@/lib/media-helper";
|
|
3
|
+
export * from "@/types/media";
|