@plugable-io/react 0.0.1 → 0.0.3
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/README.md +33 -0
- package/dist/index.d.mts +125 -0
- package/dist/index.d.ts +125 -0
- package/dist/index.js +708 -0
- package/dist/index.mjs +664 -0
- package/package.json +9 -2
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
// src/PlugableProvider.tsx
|
|
2
|
+
import { createContext, useContext, useMemo, useCallback, useRef } from "react";
|
|
3
|
+
import { BucketClient } from "@plugable-io/js";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
var PlugableContext = createContext(null);
|
|
6
|
+
function createAuthTokenGetter(authProvider, clerkJWTTemplate) {
|
|
7
|
+
switch (authProvider) {
|
|
8
|
+
case "clerk":
|
|
9
|
+
return async () => {
|
|
10
|
+
if (typeof window !== "undefined" && window.Clerk) {
|
|
11
|
+
const session = await window.Clerk.session;
|
|
12
|
+
if (!session) {
|
|
13
|
+
throw new Error("No active Clerk session found");
|
|
14
|
+
}
|
|
15
|
+
const template = clerkJWTTemplate || void 0;
|
|
16
|
+
return await session.getToken({ template });
|
|
17
|
+
}
|
|
18
|
+
throw new Error(
|
|
19
|
+
"Clerk not found. Please ensure @clerk/clerk-react is installed and ClerkProvider wraps your app, or provide a custom getToken function."
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
case "supabase":
|
|
23
|
+
return async () => {
|
|
24
|
+
if (typeof window !== "undefined" && window.supabase) {
|
|
25
|
+
const { data, error } = await window.supabase.auth.getSession();
|
|
26
|
+
if (error || !data.session) {
|
|
27
|
+
throw new Error("No active Supabase session found");
|
|
28
|
+
}
|
|
29
|
+
return data.session.access_token;
|
|
30
|
+
}
|
|
31
|
+
throw new Error(
|
|
32
|
+
"Supabase client not found. Please ensure @supabase/supabase-js is installed and initialized, or provide a custom getToken function."
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
case "firebase":
|
|
36
|
+
return async () => {
|
|
37
|
+
if (typeof window !== "undefined" && window.firebase?.auth) {
|
|
38
|
+
const user = window.firebase.auth().currentUser;
|
|
39
|
+
if (!user) {
|
|
40
|
+
throw new Error("No active Firebase user found");
|
|
41
|
+
}
|
|
42
|
+
return await user.getIdToken();
|
|
43
|
+
}
|
|
44
|
+
throw new Error(
|
|
45
|
+
"Firebase not found. Please ensure firebase is installed and initialized, or provide a custom getToken function."
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
default:
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Unknown auth provider: ${authProvider}. Please provide either a valid authProvider or a custom getToken function.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function PlugableProvider({
|
|
55
|
+
bucketId,
|
|
56
|
+
children,
|
|
57
|
+
getToken,
|
|
58
|
+
authProvider,
|
|
59
|
+
clerkJWTTemplate,
|
|
60
|
+
baseUrl
|
|
61
|
+
}) {
|
|
62
|
+
const listenersRef = useRef({});
|
|
63
|
+
const client = useMemo(() => {
|
|
64
|
+
if (!getToken && !authProvider) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
"Either getToken function or authProvider must be provided to PlugableProvider"
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const tokenGetter = getToken || createAuthTokenGetter(authProvider, clerkJWTTemplate);
|
|
70
|
+
const client_ = new BucketClient({
|
|
71
|
+
bucketId,
|
|
72
|
+
getToken: tokenGetter,
|
|
73
|
+
baseUrl
|
|
74
|
+
});
|
|
75
|
+
return { client: client_, tokenGetter };
|
|
76
|
+
}, [bucketId, getToken, authProvider, clerkJWTTemplate, baseUrl]);
|
|
77
|
+
const on = useCallback((event, handler) => {
|
|
78
|
+
if (!listenersRef.current[event]) {
|
|
79
|
+
listenersRef.current[event] = /* @__PURE__ */ new Set();
|
|
80
|
+
}
|
|
81
|
+
listenersRef.current[event].add(handler);
|
|
82
|
+
return () => {
|
|
83
|
+
listenersRef.current[event]?.delete(handler);
|
|
84
|
+
};
|
|
85
|
+
}, []);
|
|
86
|
+
const emit = useCallback((event, data) => {
|
|
87
|
+
listenersRef.current[event]?.forEach((handler) => handler(data));
|
|
88
|
+
}, []);
|
|
89
|
+
const value = useMemo(
|
|
90
|
+
() => ({
|
|
91
|
+
client: client.client,
|
|
92
|
+
bucketId,
|
|
93
|
+
on,
|
|
94
|
+
emit,
|
|
95
|
+
getToken: client.tokenGetter,
|
|
96
|
+
baseUrl
|
|
97
|
+
}),
|
|
98
|
+
[client, bucketId, on, emit, baseUrl]
|
|
99
|
+
);
|
|
100
|
+
return /* @__PURE__ */ jsx(PlugableContext.Provider, { value, children });
|
|
101
|
+
}
|
|
102
|
+
function usePlugable() {
|
|
103
|
+
const context = useContext(PlugableContext);
|
|
104
|
+
if (!context) {
|
|
105
|
+
throw new Error("usePlugable must be used within a PlugableProvider");
|
|
106
|
+
}
|
|
107
|
+
return context;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/components/Dropzone.tsx
|
|
111
|
+
import React, { useCallback as useCallback2, useState } from "react";
|
|
112
|
+
import { BucketClient as BucketClient2 } from "@plugable-io/js";
|
|
113
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
114
|
+
function Dropzone({
|
|
115
|
+
bucketId: _bucketId,
|
|
116
|
+
metadata,
|
|
117
|
+
onUploadComplete,
|
|
118
|
+
onUploadError,
|
|
119
|
+
onProgressUpdate,
|
|
120
|
+
accept,
|
|
121
|
+
maxFiles,
|
|
122
|
+
children,
|
|
123
|
+
className,
|
|
124
|
+
style
|
|
125
|
+
}) {
|
|
126
|
+
const { client: defaultClient, emit, getToken, baseUrl } = usePlugable();
|
|
127
|
+
const client = React.useMemo(() => {
|
|
128
|
+
if (_bucketId) {
|
|
129
|
+
return new BucketClient2({
|
|
130
|
+
bucketId: _bucketId,
|
|
131
|
+
getToken,
|
|
132
|
+
baseUrl
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return defaultClient;
|
|
136
|
+
}, [_bucketId, defaultClient, getToken, baseUrl]);
|
|
137
|
+
const [isDragActive, setIsDragActive] = useState(false);
|
|
138
|
+
const [isUploading, setIsUploading] = useState(false);
|
|
139
|
+
const [uploadProgress, setUploadProgress] = useState({});
|
|
140
|
+
const [uploadedFiles, setUploadedFiles] = useState([]);
|
|
141
|
+
const fileInputRef = React.useRef(null);
|
|
142
|
+
const uploadFiles = useCallback2(
|
|
143
|
+
async (files) => {
|
|
144
|
+
const fileArray = Array.from(files);
|
|
145
|
+
const filesToUpload = maxFiles ? fileArray.slice(0, maxFiles) : fileArray;
|
|
146
|
+
setIsUploading(true);
|
|
147
|
+
setUploadProgress({});
|
|
148
|
+
const uploadPromises = filesToUpload.map(async (file) => {
|
|
149
|
+
const options = {
|
|
150
|
+
metadata,
|
|
151
|
+
onProgress: (progress) => {
|
|
152
|
+
setUploadProgress((prev) => ({
|
|
153
|
+
...prev,
|
|
154
|
+
[file.name]: progress
|
|
155
|
+
}));
|
|
156
|
+
onProgressUpdate?.(file.name, progress);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
try {
|
|
160
|
+
const uploadedFile = await client.upload(file, options);
|
|
161
|
+
return uploadedFile;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error(`Failed to upload ${file.name}:`, error);
|
|
164
|
+
onUploadError?.(error);
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
const results = await Promise.all(uploadPromises);
|
|
169
|
+
const successfulUploads = results.filter((f) => f !== null);
|
|
170
|
+
setUploadedFiles((prev) => [...prev, ...successfulUploads]);
|
|
171
|
+
setIsUploading(false);
|
|
172
|
+
setUploadProgress({});
|
|
173
|
+
if (successfulUploads.length > 0) {
|
|
174
|
+
successfulUploads.forEach((file) => emit("file.uploaded", file));
|
|
175
|
+
onUploadComplete?.(successfulUploads);
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
[client, metadata, maxFiles, onUploadComplete, onUploadError, onProgressUpdate, emit]
|
|
179
|
+
);
|
|
180
|
+
const handleDrop = useCallback2(
|
|
181
|
+
(e) => {
|
|
182
|
+
e.preventDefault();
|
|
183
|
+
e.stopPropagation();
|
|
184
|
+
setIsDragActive(false);
|
|
185
|
+
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
|
186
|
+
uploadFiles(e.dataTransfer.files);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
[uploadFiles]
|
|
190
|
+
);
|
|
191
|
+
const handleDragOver = useCallback2((e) => {
|
|
192
|
+
e.preventDefault();
|
|
193
|
+
e.stopPropagation();
|
|
194
|
+
setIsDragActive(true);
|
|
195
|
+
}, []);
|
|
196
|
+
const handleDragLeave = useCallback2((e) => {
|
|
197
|
+
e.preventDefault();
|
|
198
|
+
e.stopPropagation();
|
|
199
|
+
setIsDragActive(false);
|
|
200
|
+
}, []);
|
|
201
|
+
const handleFileInputChange = useCallback2(
|
|
202
|
+
(e) => {
|
|
203
|
+
if (e.target.files && e.target.files.length > 0) {
|
|
204
|
+
uploadFiles(e.target.files);
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
[uploadFiles]
|
|
208
|
+
);
|
|
209
|
+
const openFileDialog = useCallback2(() => {
|
|
210
|
+
fileInputRef.current?.click();
|
|
211
|
+
}, []);
|
|
212
|
+
const renderProps = {
|
|
213
|
+
isDragActive,
|
|
214
|
+
isUploading,
|
|
215
|
+
uploadProgress,
|
|
216
|
+
openFileDialog,
|
|
217
|
+
uploadedFiles
|
|
218
|
+
};
|
|
219
|
+
if (children) {
|
|
220
|
+
return /* @__PURE__ */ jsxs(
|
|
221
|
+
"div",
|
|
222
|
+
{
|
|
223
|
+
onDrop: handleDrop,
|
|
224
|
+
onDragOver: handleDragOver,
|
|
225
|
+
onDragLeave: handleDragLeave,
|
|
226
|
+
className,
|
|
227
|
+
style,
|
|
228
|
+
children: [
|
|
229
|
+
/* @__PURE__ */ jsx2(
|
|
230
|
+
"input",
|
|
231
|
+
{
|
|
232
|
+
ref: fileInputRef,
|
|
233
|
+
type: "file",
|
|
234
|
+
multiple: !maxFiles || maxFiles > 1,
|
|
235
|
+
accept,
|
|
236
|
+
onChange: handleFileInputChange,
|
|
237
|
+
style: { display: "none" }
|
|
238
|
+
}
|
|
239
|
+
),
|
|
240
|
+
children(renderProps)
|
|
241
|
+
]
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
return /* @__PURE__ */ jsxs(
|
|
246
|
+
"div",
|
|
247
|
+
{
|
|
248
|
+
onDrop: handleDrop,
|
|
249
|
+
onDragOver: handleDragOver,
|
|
250
|
+
onDragLeave: handleDragLeave,
|
|
251
|
+
onClick: openFileDialog,
|
|
252
|
+
className,
|
|
253
|
+
style: {
|
|
254
|
+
border: `2px dashed ${isDragActive ? "#0070f3" : "#ccc"}`,
|
|
255
|
+
borderRadius: "8px",
|
|
256
|
+
padding: "40px 20px",
|
|
257
|
+
textAlign: "center",
|
|
258
|
+
cursor: "pointer",
|
|
259
|
+
backgroundColor: isDragActive ? "#f0f8ff" : "#fafafa",
|
|
260
|
+
transition: "all 0.2s ease",
|
|
261
|
+
...style
|
|
262
|
+
},
|
|
263
|
+
children: [
|
|
264
|
+
/* @__PURE__ */ jsx2(
|
|
265
|
+
"input",
|
|
266
|
+
{
|
|
267
|
+
ref: fileInputRef,
|
|
268
|
+
type: "file",
|
|
269
|
+
multiple: !maxFiles || maxFiles > 1,
|
|
270
|
+
accept,
|
|
271
|
+
onChange: handleFileInputChange,
|
|
272
|
+
style: { display: "none" }
|
|
273
|
+
}
|
|
274
|
+
),
|
|
275
|
+
isUploading ? /* @__PURE__ */ jsxs("div", { children: [
|
|
276
|
+
/* @__PURE__ */ jsx2("p", { style: { margin: 0, fontWeight: "bold", color: "#333" }, children: "Uploading..." }),
|
|
277
|
+
/* @__PURE__ */ jsx2("div", { style: { marginTop: "16px" }, children: Object.entries(uploadProgress).map(([fileName, progress]) => /* @__PURE__ */ jsxs("div", { style: { marginBottom: "8px" }, children: [
|
|
278
|
+
/* @__PURE__ */ jsxs("div", { style: { fontSize: "14px", color: "#666", marginBottom: "4px" }, children: [
|
|
279
|
+
fileName,
|
|
280
|
+
": ",
|
|
281
|
+
progress,
|
|
282
|
+
"%"
|
|
283
|
+
] }),
|
|
284
|
+
/* @__PURE__ */ jsx2(
|
|
285
|
+
"div",
|
|
286
|
+
{
|
|
287
|
+
style: {
|
|
288
|
+
width: "100%",
|
|
289
|
+
height: "8px",
|
|
290
|
+
backgroundColor: "#e0e0e0",
|
|
291
|
+
borderRadius: "4px",
|
|
292
|
+
overflow: "hidden"
|
|
293
|
+
},
|
|
294
|
+
children: /* @__PURE__ */ jsx2(
|
|
295
|
+
"div",
|
|
296
|
+
{
|
|
297
|
+
style: {
|
|
298
|
+
width: `${progress}%`,
|
|
299
|
+
height: "100%",
|
|
300
|
+
backgroundColor: "#0070f3",
|
|
301
|
+
transition: "width 0.3s ease"
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
)
|
|
307
|
+
] }, fileName)) })
|
|
308
|
+
] }) : /* @__PURE__ */ jsxs("div", { children: [
|
|
309
|
+
/* @__PURE__ */ jsx2("p", { style: { margin: 0, fontWeight: "bold", fontSize: "16px", color: "#333" }, children: isDragActive ? "Drop files here" : "Drag and drop files here" }),
|
|
310
|
+
/* @__PURE__ */ jsx2("p", { style: { margin: "8px 0 0 0", fontSize: "14px", color: "#666" }, children: "or click to select files" }),
|
|
311
|
+
maxFiles && maxFiles > 1 && /* @__PURE__ */ jsxs("p", { style: { margin: "4px 0 0 0", fontSize: "12px", color: "#999" }, children: [
|
|
312
|
+
"(Maximum ",
|
|
313
|
+
maxFiles,
|
|
314
|
+
" files)"
|
|
315
|
+
] })
|
|
316
|
+
] })
|
|
317
|
+
]
|
|
318
|
+
}
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/hooks/useFiles.ts
|
|
323
|
+
import { useState as useState2, useCallback as useCallback3, useEffect, useMemo as useMemo2, useRef as useRef2 } from "react";
|
|
324
|
+
function useFiles({
|
|
325
|
+
metadata,
|
|
326
|
+
startPage = 1,
|
|
327
|
+
perPage = 20,
|
|
328
|
+
mediaType,
|
|
329
|
+
autoLoad = true,
|
|
330
|
+
orderBy,
|
|
331
|
+
orderDirection
|
|
332
|
+
} = {}) {
|
|
333
|
+
const { client, on } = usePlugable();
|
|
334
|
+
const [files, setFiles] = useState2([]);
|
|
335
|
+
const [isLoading, setIsLoading] = useState2(false);
|
|
336
|
+
const [page, setPage] = useState2(startPage);
|
|
337
|
+
const [hasNext, setHasNext] = useState2(false);
|
|
338
|
+
const metadataKey = JSON.stringify(metadata);
|
|
339
|
+
const stableMetadata = useMemo2(() => metadata, [metadataKey]);
|
|
340
|
+
const loadedParamsRef = useRef2(null);
|
|
341
|
+
const paramsKeyWithPage = useMemo2(() => JSON.stringify({
|
|
342
|
+
metadata: stableMetadata,
|
|
343
|
+
mediaType,
|
|
344
|
+
perPage,
|
|
345
|
+
orderBy,
|
|
346
|
+
orderDirection,
|
|
347
|
+
page
|
|
348
|
+
}), [stableMetadata, mediaType, perPage, orderBy, orderDirection, page]);
|
|
349
|
+
const fetchFiles = useCallback3(async (pageNum) => {
|
|
350
|
+
setIsLoading(true);
|
|
351
|
+
try {
|
|
352
|
+
const options = {
|
|
353
|
+
metadata: stableMetadata,
|
|
354
|
+
media_type: mediaType,
|
|
355
|
+
page: pageNum,
|
|
356
|
+
per_page: perPage,
|
|
357
|
+
with_download_url: true,
|
|
358
|
+
order_by: orderBy,
|
|
359
|
+
order_direction: orderDirection
|
|
360
|
+
};
|
|
361
|
+
const response = await client.list(options);
|
|
362
|
+
setFiles(response.files);
|
|
363
|
+
setHasNext(response.paging.has_next_page);
|
|
364
|
+
const currentParamsKey = JSON.stringify({
|
|
365
|
+
metadata: stableMetadata,
|
|
366
|
+
mediaType,
|
|
367
|
+
perPage,
|
|
368
|
+
orderBy,
|
|
369
|
+
orderDirection,
|
|
370
|
+
page: pageNum
|
|
371
|
+
});
|
|
372
|
+
loadedParamsRef.current = currentParamsKey;
|
|
373
|
+
} catch (err) {
|
|
374
|
+
console.error("Failed to load files:", err);
|
|
375
|
+
setFiles([]);
|
|
376
|
+
setHasNext(false);
|
|
377
|
+
} finally {
|
|
378
|
+
setIsLoading(false);
|
|
379
|
+
}
|
|
380
|
+
}, [client, stableMetadata, mediaType, perPage, orderBy, orderDirection]);
|
|
381
|
+
useEffect(() => {
|
|
382
|
+
const hasLoadedForParams = loadedParamsRef.current === paramsKeyWithPage;
|
|
383
|
+
const shouldLoad = autoLoad || !hasLoadedForParams && files.length === 0 && !isLoading;
|
|
384
|
+
if (shouldLoad) {
|
|
385
|
+
fetchFiles(page);
|
|
386
|
+
}
|
|
387
|
+
}, [fetchFiles, page, autoLoad, paramsKeyWithPage, isLoading]);
|
|
388
|
+
useEffect(() => {
|
|
389
|
+
const unsubscribe = on("file.uploaded", () => {
|
|
390
|
+
fetchFiles(page);
|
|
391
|
+
});
|
|
392
|
+
return unsubscribe;
|
|
393
|
+
}, [on, fetchFiles, page]);
|
|
394
|
+
const loadNextPage = useCallback3(() => {
|
|
395
|
+
if (hasNext) {
|
|
396
|
+
setPage((p) => p + 1);
|
|
397
|
+
}
|
|
398
|
+
}, [hasNext]);
|
|
399
|
+
const loadPreviousPage = useCallback3(() => {
|
|
400
|
+
setPage((p) => Math.max(1, p - 1));
|
|
401
|
+
}, []);
|
|
402
|
+
const refresh = useCallback3(async () => {
|
|
403
|
+
await fetchFiles(page);
|
|
404
|
+
}, [fetchFiles, page]);
|
|
405
|
+
return {
|
|
406
|
+
files,
|
|
407
|
+
isLoading,
|
|
408
|
+
pagination: {
|
|
409
|
+
current: page,
|
|
410
|
+
hasNext,
|
|
411
|
+
hasPrevious: page > 1,
|
|
412
|
+
loadNextPage,
|
|
413
|
+
loadPreviousPage
|
|
414
|
+
},
|
|
415
|
+
setPage,
|
|
416
|
+
refresh
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/components/FileList.tsx
|
|
421
|
+
import { Fragment, jsx as jsx3 } from "react/jsx-runtime";
|
|
422
|
+
function FileList({
|
|
423
|
+
metadata,
|
|
424
|
+
mediaType,
|
|
425
|
+
perPage = 20,
|
|
426
|
+
autoLoad = true,
|
|
427
|
+
startPage = 1,
|
|
428
|
+
children
|
|
429
|
+
}) {
|
|
430
|
+
const { files, isLoading, pagination, refresh } = useFiles({
|
|
431
|
+
metadata,
|
|
432
|
+
mediaType,
|
|
433
|
+
perPage,
|
|
434
|
+
autoLoad,
|
|
435
|
+
startPage
|
|
436
|
+
});
|
|
437
|
+
const renderProps = {
|
|
438
|
+
files,
|
|
439
|
+
isLoading,
|
|
440
|
+
hasMore: pagination.hasNext,
|
|
441
|
+
loadMore: pagination.loadNextPage,
|
|
442
|
+
refresh,
|
|
443
|
+
error: null,
|
|
444
|
+
pagination
|
|
445
|
+
};
|
|
446
|
+
return /* @__PURE__ */ jsx3(Fragment, { children: children(renderProps) });
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/components/FileImage.tsx
|
|
450
|
+
import { useEffect as useEffect2, useState as useState3 } from "react";
|
|
451
|
+
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
452
|
+
var imageCache = /* @__PURE__ */ new Map();
|
|
453
|
+
function FileImage({
|
|
454
|
+
file,
|
|
455
|
+
width,
|
|
456
|
+
height,
|
|
457
|
+
objectFit = "cover",
|
|
458
|
+
borderRadius,
|
|
459
|
+
alt,
|
|
460
|
+
className,
|
|
461
|
+
style,
|
|
462
|
+
onLoad,
|
|
463
|
+
onError
|
|
464
|
+
}) {
|
|
465
|
+
const [imageSrc, setImageSrc] = useState3(null);
|
|
466
|
+
const [isLoading, setIsLoading] = useState3(true);
|
|
467
|
+
const [error, setError] = useState3(null);
|
|
468
|
+
useEffect2(() => {
|
|
469
|
+
let isMounted = true;
|
|
470
|
+
let objectUrl = null;
|
|
471
|
+
const loadImage = async () => {
|
|
472
|
+
try {
|
|
473
|
+
const cacheKey = `${file.id}-${file.checksum}`;
|
|
474
|
+
const cached = imageCache.get(cacheKey);
|
|
475
|
+
if (cached) {
|
|
476
|
+
if (isMounted) {
|
|
477
|
+
setImageSrc(cached);
|
|
478
|
+
setIsLoading(false);
|
|
479
|
+
}
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (file.download_url) {
|
|
483
|
+
const response = await fetch(file.download_url, {
|
|
484
|
+
headers: {
|
|
485
|
+
"Cache-Control": "private, max-age=31536000",
|
|
486
|
+
"If-None-Match": file.checksum
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
if (!response.ok) {
|
|
490
|
+
throw new Error(`Failed to fetch image: ${response.statusText}`);
|
|
491
|
+
}
|
|
492
|
+
const blob = await response.blob();
|
|
493
|
+
objectUrl = URL.createObjectURL(blob);
|
|
494
|
+
imageCache.set(cacheKey, objectUrl);
|
|
495
|
+
if (isMounted) {
|
|
496
|
+
setImageSrc(objectUrl);
|
|
497
|
+
setIsLoading(false);
|
|
498
|
+
}
|
|
499
|
+
} else {
|
|
500
|
+
throw new Error("No download URL available for file");
|
|
501
|
+
}
|
|
502
|
+
} catch (err) {
|
|
503
|
+
const error2 = err;
|
|
504
|
+
if (isMounted) {
|
|
505
|
+
setError(error2);
|
|
506
|
+
setIsLoading(false);
|
|
507
|
+
onError?.(error2);
|
|
508
|
+
}
|
|
509
|
+
console.error("Failed to load image:", err);
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
loadImage();
|
|
513
|
+
return () => {
|
|
514
|
+
isMounted = false;
|
|
515
|
+
};
|
|
516
|
+
}, [file.id, file.checksum, file.download_url, onError]);
|
|
517
|
+
const handleLoad = () => {
|
|
518
|
+
setIsLoading(false);
|
|
519
|
+
onLoad?.();
|
|
520
|
+
};
|
|
521
|
+
const handleError = () => {
|
|
522
|
+
const err = new Error("Image failed to load");
|
|
523
|
+
setError(err);
|
|
524
|
+
setIsLoading(false);
|
|
525
|
+
onError?.(err);
|
|
526
|
+
};
|
|
527
|
+
const imageStyle = {
|
|
528
|
+
width: width || "100%",
|
|
529
|
+
height: height || "auto",
|
|
530
|
+
objectFit,
|
|
531
|
+
borderRadius: borderRadius || 0,
|
|
532
|
+
...style
|
|
533
|
+
};
|
|
534
|
+
if (error) {
|
|
535
|
+
return /* @__PURE__ */ jsx4(
|
|
536
|
+
"div",
|
|
537
|
+
{
|
|
538
|
+
className,
|
|
539
|
+
style: {
|
|
540
|
+
...imageStyle,
|
|
541
|
+
display: "flex",
|
|
542
|
+
alignItems: "center",
|
|
543
|
+
justifyContent: "center",
|
|
544
|
+
backgroundColor: "#f0f0f0",
|
|
545
|
+
color: "#999",
|
|
546
|
+
fontSize: "14px"
|
|
547
|
+
},
|
|
548
|
+
children: "Failed to load image"
|
|
549
|
+
}
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
if (isLoading || !imageSrc) {
|
|
553
|
+
return /* @__PURE__ */ jsxs2(
|
|
554
|
+
"div",
|
|
555
|
+
{
|
|
556
|
+
className,
|
|
557
|
+
style: {
|
|
558
|
+
...imageStyle,
|
|
559
|
+
display: "flex",
|
|
560
|
+
alignItems: "center",
|
|
561
|
+
justifyContent: "center",
|
|
562
|
+
backgroundColor: "#f0f0f0"
|
|
563
|
+
},
|
|
564
|
+
children: [
|
|
565
|
+
/* @__PURE__ */ jsx4(
|
|
566
|
+
"div",
|
|
567
|
+
{
|
|
568
|
+
style: {
|
|
569
|
+
width: "40px",
|
|
570
|
+
height: "40px",
|
|
571
|
+
border: "3px solid #e0e0e0",
|
|
572
|
+
borderTop: "3px solid #0070f3",
|
|
573
|
+
borderRadius: "50%",
|
|
574
|
+
animation: "spin 1s linear infinite"
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
),
|
|
578
|
+
/* @__PURE__ */ jsx4("style", { children: `
|
|
579
|
+
@keyframes spin {
|
|
580
|
+
0% { transform: rotate(0deg); }
|
|
581
|
+
100% { transform: rotate(360deg); }
|
|
582
|
+
}
|
|
583
|
+
` })
|
|
584
|
+
]
|
|
585
|
+
}
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
return /* @__PURE__ */ jsx4(
|
|
589
|
+
"img",
|
|
590
|
+
{
|
|
591
|
+
src: imageSrc,
|
|
592
|
+
alt: alt || file.name,
|
|
593
|
+
className,
|
|
594
|
+
style: imageStyle,
|
|
595
|
+
onLoad: handleLoad,
|
|
596
|
+
onError: handleError
|
|
597
|
+
}
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
function clearImageCache() {
|
|
601
|
+
imageCache.forEach((url) => URL.revokeObjectURL(url));
|
|
602
|
+
imageCache.clear();
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// src/components/FilePreview.tsx
|
|
606
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
607
|
+
function FilePreview({
|
|
608
|
+
file,
|
|
609
|
+
width = 80,
|
|
610
|
+
height = 80,
|
|
611
|
+
className,
|
|
612
|
+
style,
|
|
613
|
+
objectFit = "cover",
|
|
614
|
+
showExtension = true,
|
|
615
|
+
renderNonImage
|
|
616
|
+
}) {
|
|
617
|
+
const isImage = file.content_type.startsWith("image/");
|
|
618
|
+
const containerStyle = {
|
|
619
|
+
width,
|
|
620
|
+
height,
|
|
621
|
+
borderRadius: 4,
|
|
622
|
+
overflow: "hidden",
|
|
623
|
+
backgroundColor: "#f5f5f5",
|
|
624
|
+
display: "flex",
|
|
625
|
+
alignItems: "center",
|
|
626
|
+
justifyContent: "center",
|
|
627
|
+
border: "1px solid #eee",
|
|
628
|
+
...style
|
|
629
|
+
};
|
|
630
|
+
if (isImage) {
|
|
631
|
+
return /* @__PURE__ */ jsx5(
|
|
632
|
+
FileImage,
|
|
633
|
+
{
|
|
634
|
+
file,
|
|
635
|
+
width,
|
|
636
|
+
height,
|
|
637
|
+
objectFit,
|
|
638
|
+
className,
|
|
639
|
+
style,
|
|
640
|
+
borderRadius: 4
|
|
641
|
+
}
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
if (renderNonImage) {
|
|
645
|
+
return /* @__PURE__ */ jsx5("div", { className, style: containerStyle, children: renderNonImage(file) });
|
|
646
|
+
}
|
|
647
|
+
const extension = file.name.split(".").pop()?.toUpperCase() || "FILE";
|
|
648
|
+
return /* @__PURE__ */ jsx5("div", { className, style: containerStyle, children: showExtension && /* @__PURE__ */ jsx5("span", { style: {
|
|
649
|
+
fontSize: "12px",
|
|
650
|
+
fontWeight: "bold",
|
|
651
|
+
color: "#666",
|
|
652
|
+
textTransform: "uppercase"
|
|
653
|
+
}, children: extension }) });
|
|
654
|
+
}
|
|
655
|
+
export {
|
|
656
|
+
Dropzone,
|
|
657
|
+
FileImage,
|
|
658
|
+
FileList,
|
|
659
|
+
FilePreview,
|
|
660
|
+
PlugableProvider,
|
|
661
|
+
clearImageCache,
|
|
662
|
+
useFiles,
|
|
663
|
+
usePlugable
|
|
664
|
+
};
|
package/package.json
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plugable-io/react",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "React components and hooks for Plugable File Management API",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
8
15
|
"files": [
|
|
9
16
|
"dist"
|
|
10
17
|
],
|
|
@@ -36,7 +43,7 @@
|
|
|
36
43
|
"react-dom": ">=16.8.0"
|
|
37
44
|
},
|
|
38
45
|
"dependencies": {
|
|
39
|
-
"@plugable-io/js": "^0.0.
|
|
46
|
+
"@plugable-io/js": "^0.0.9"
|
|
40
47
|
},
|
|
41
48
|
"devDependencies": {
|
|
42
49
|
"@types/node": "^20.0.0",
|