@plugable-io/react 0.0.1 → 0.0.2

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/dist/index.mjs ADDED
@@ -0,0 +1,640 @@
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 } from "react";
324
+ function useFiles({
325
+ metadata,
326
+ startPage = 1,
327
+ perPage = 20,
328
+ mediaType,
329
+ autoLoad = true
330
+ } = {}) {
331
+ const { client, on } = usePlugable();
332
+ const [files, setFiles] = useState2([]);
333
+ const [isLoading, setIsLoading] = useState2(false);
334
+ const [page, setPage] = useState2(startPage);
335
+ const [hasNext, setHasNext] = useState2(false);
336
+ const metadataKey = JSON.stringify(metadata);
337
+ const stableMetadata = useMemo2(() => metadata, [metadataKey]);
338
+ const fetchFiles = useCallback3(async (pageNum) => {
339
+ setIsLoading(true);
340
+ try {
341
+ const options = {
342
+ metadata: stableMetadata,
343
+ media_type: mediaType,
344
+ page: pageNum,
345
+ per_page: perPage,
346
+ with_download_url: true
347
+ };
348
+ const response = await client.list(options);
349
+ setFiles(response.files);
350
+ setHasNext(response.paging.has_next_page);
351
+ } catch (err) {
352
+ console.error("Failed to load files:", err);
353
+ setFiles([]);
354
+ setHasNext(false);
355
+ } finally {
356
+ setIsLoading(false);
357
+ }
358
+ }, [client, stableMetadata, mediaType, perPage]);
359
+ useEffect(() => {
360
+ if (autoLoad) {
361
+ fetchFiles(page);
362
+ }
363
+ }, [fetchFiles, page, autoLoad]);
364
+ useEffect(() => {
365
+ const unsubscribe = on("file.uploaded", () => {
366
+ fetchFiles(page);
367
+ });
368
+ return unsubscribe;
369
+ }, [on, fetchFiles, page]);
370
+ const loadNextPage = useCallback3(() => {
371
+ if (hasNext) {
372
+ setPage((p) => p + 1);
373
+ }
374
+ }, [hasNext]);
375
+ const loadPreviousPage = useCallback3(() => {
376
+ setPage((p) => Math.max(1, p - 1));
377
+ }, []);
378
+ const refresh = useCallback3(async () => {
379
+ await fetchFiles(page);
380
+ }, [fetchFiles, page]);
381
+ return {
382
+ files,
383
+ isLoading,
384
+ pagination: {
385
+ current: page,
386
+ hasNext,
387
+ hasPrevious: page > 1,
388
+ loadNextPage,
389
+ loadPreviousPage
390
+ },
391
+ setPage,
392
+ refresh
393
+ };
394
+ }
395
+
396
+ // src/components/FileList.tsx
397
+ import { Fragment, jsx as jsx3 } from "react/jsx-runtime";
398
+ function FileList({
399
+ metadata,
400
+ mediaType,
401
+ perPage = 20,
402
+ autoLoad = true,
403
+ startPage = 1,
404
+ children
405
+ }) {
406
+ const { files, isLoading, pagination, refresh } = useFiles({
407
+ metadata,
408
+ mediaType,
409
+ perPage,
410
+ autoLoad,
411
+ startPage
412
+ });
413
+ const renderProps = {
414
+ files,
415
+ isLoading,
416
+ hasMore: pagination.hasNext,
417
+ loadMore: pagination.loadNextPage,
418
+ refresh,
419
+ error: null,
420
+ pagination
421
+ };
422
+ return /* @__PURE__ */ jsx3(Fragment, { children: children(renderProps) });
423
+ }
424
+
425
+ // src/components/FileImage.tsx
426
+ import { useEffect as useEffect2, useState as useState3 } from "react";
427
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
428
+ var imageCache = /* @__PURE__ */ new Map();
429
+ function FileImage({
430
+ file,
431
+ width,
432
+ height,
433
+ objectFit = "cover",
434
+ borderRadius,
435
+ alt,
436
+ className,
437
+ style,
438
+ onLoad,
439
+ onError
440
+ }) {
441
+ const [imageSrc, setImageSrc] = useState3(null);
442
+ const [isLoading, setIsLoading] = useState3(true);
443
+ const [error, setError] = useState3(null);
444
+ useEffect2(() => {
445
+ let isMounted = true;
446
+ let objectUrl = null;
447
+ const loadImage = async () => {
448
+ try {
449
+ const cacheKey = `${file.id}-${file.checksum}`;
450
+ const cached = imageCache.get(cacheKey);
451
+ if (cached) {
452
+ if (isMounted) {
453
+ setImageSrc(cached);
454
+ setIsLoading(false);
455
+ }
456
+ return;
457
+ }
458
+ if (file.download_url) {
459
+ const response = await fetch(file.download_url, {
460
+ headers: {
461
+ "Cache-Control": "private, max-age=31536000",
462
+ "If-None-Match": file.checksum
463
+ }
464
+ });
465
+ if (!response.ok) {
466
+ throw new Error(`Failed to fetch image: ${response.statusText}`);
467
+ }
468
+ const blob = await response.blob();
469
+ objectUrl = URL.createObjectURL(blob);
470
+ imageCache.set(cacheKey, objectUrl);
471
+ if (isMounted) {
472
+ setImageSrc(objectUrl);
473
+ setIsLoading(false);
474
+ }
475
+ } else {
476
+ throw new Error("No download URL available for file");
477
+ }
478
+ } catch (err) {
479
+ const error2 = err;
480
+ if (isMounted) {
481
+ setError(error2);
482
+ setIsLoading(false);
483
+ onError?.(error2);
484
+ }
485
+ console.error("Failed to load image:", err);
486
+ }
487
+ };
488
+ loadImage();
489
+ return () => {
490
+ isMounted = false;
491
+ };
492
+ }, [file.id, file.checksum, file.download_url, onError]);
493
+ const handleLoad = () => {
494
+ setIsLoading(false);
495
+ onLoad?.();
496
+ };
497
+ const handleError = () => {
498
+ const err = new Error("Image failed to load");
499
+ setError(err);
500
+ setIsLoading(false);
501
+ onError?.(err);
502
+ };
503
+ const imageStyle = {
504
+ width: width || "100%",
505
+ height: height || "auto",
506
+ objectFit,
507
+ borderRadius: borderRadius || 0,
508
+ ...style
509
+ };
510
+ if (error) {
511
+ return /* @__PURE__ */ jsx4(
512
+ "div",
513
+ {
514
+ className,
515
+ style: {
516
+ ...imageStyle,
517
+ display: "flex",
518
+ alignItems: "center",
519
+ justifyContent: "center",
520
+ backgroundColor: "#f0f0f0",
521
+ color: "#999",
522
+ fontSize: "14px"
523
+ },
524
+ children: "Failed to load image"
525
+ }
526
+ );
527
+ }
528
+ if (isLoading || !imageSrc) {
529
+ return /* @__PURE__ */ jsxs2(
530
+ "div",
531
+ {
532
+ className,
533
+ style: {
534
+ ...imageStyle,
535
+ display: "flex",
536
+ alignItems: "center",
537
+ justifyContent: "center",
538
+ backgroundColor: "#f0f0f0"
539
+ },
540
+ children: [
541
+ /* @__PURE__ */ jsx4(
542
+ "div",
543
+ {
544
+ style: {
545
+ width: "40px",
546
+ height: "40px",
547
+ border: "3px solid #e0e0e0",
548
+ borderTop: "3px solid #0070f3",
549
+ borderRadius: "50%",
550
+ animation: "spin 1s linear infinite"
551
+ }
552
+ }
553
+ ),
554
+ /* @__PURE__ */ jsx4("style", { children: `
555
+ @keyframes spin {
556
+ 0% { transform: rotate(0deg); }
557
+ 100% { transform: rotate(360deg); }
558
+ }
559
+ ` })
560
+ ]
561
+ }
562
+ );
563
+ }
564
+ return /* @__PURE__ */ jsx4(
565
+ "img",
566
+ {
567
+ src: imageSrc,
568
+ alt: alt || file.name,
569
+ className,
570
+ style: imageStyle,
571
+ onLoad: handleLoad,
572
+ onError: handleError
573
+ }
574
+ );
575
+ }
576
+ function clearImageCache() {
577
+ imageCache.forEach((url) => URL.revokeObjectURL(url));
578
+ imageCache.clear();
579
+ }
580
+
581
+ // src/components/FilePreview.tsx
582
+ import { jsx as jsx5 } from "react/jsx-runtime";
583
+ function FilePreview({
584
+ file,
585
+ width = 80,
586
+ height = 80,
587
+ className,
588
+ style,
589
+ objectFit = "cover",
590
+ showExtension = true,
591
+ renderNonImage
592
+ }) {
593
+ const isImage = file.content_type.startsWith("image/");
594
+ const containerStyle = {
595
+ width,
596
+ height,
597
+ borderRadius: 4,
598
+ overflow: "hidden",
599
+ backgroundColor: "#f5f5f5",
600
+ display: "flex",
601
+ alignItems: "center",
602
+ justifyContent: "center",
603
+ border: "1px solid #eee",
604
+ ...style
605
+ };
606
+ if (isImage) {
607
+ return /* @__PURE__ */ jsx5(
608
+ FileImage,
609
+ {
610
+ file,
611
+ width,
612
+ height,
613
+ objectFit,
614
+ className,
615
+ style,
616
+ borderRadius: 4
617
+ }
618
+ );
619
+ }
620
+ if (renderNonImage) {
621
+ return /* @__PURE__ */ jsx5("div", { className, style: containerStyle, children: renderNonImage(file) });
622
+ }
623
+ const extension = file.name.split(".").pop()?.toUpperCase() || "FILE";
624
+ return /* @__PURE__ */ jsx5("div", { className, style: containerStyle, children: showExtension && /* @__PURE__ */ jsx5("span", { style: {
625
+ fontSize: "12px",
626
+ fontWeight: "bold",
627
+ color: "#666",
628
+ textTransform: "uppercase"
629
+ }, children: extension }) });
630
+ }
631
+ export {
632
+ Dropzone,
633
+ FileImage,
634
+ FileList,
635
+ FilePreview,
636
+ PlugableProvider,
637
+ clearImageCache,
638
+ useFiles,
639
+ usePlugable
640
+ };
package/package.json CHANGED
@@ -1,10 +1,17 @@
1
1
  {
2
2
  "name": "@plugable-io/react",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
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
  ],