squarefi-bff-api-module 1.30.10 → 1.31.0

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.
@@ -0,0 +1,529 @@
1
+ # Frontend Storage Module Guide
2
+
3
+ A simple and quick guide for using the file upload module in React applications.
4
+
5
+ ## 🚀 Quick Start
6
+
7
+ ### 1. Environment Variables Setup
8
+
9
+ ```bash
10
+ # Copy env.example to .env and fill in your values
11
+ # See env.example in repository root for all available variables
12
+
13
+ # .env or .env.local
14
+ SUPABASE_URL=https://your-project.supabase.co
15
+ SUPABASE_PUBLIC_KEY=your-anon-key
16
+ ```
17
+
18
+ ### 2. Supabase Setup (one time)
19
+
20
+ 1. Open Supabase Dashboard → SQL Editor
21
+ 2. Copy and execute the content of `scripts/supabase-storage-setup.sql`
22
+ 3. Edit the `is_super_admin()` function to match your user schema
23
+
24
+ ### 3. Done! Use the hooks
25
+
26
+ ## 📤 Upload Files with Hook
27
+
28
+ ```tsx
29
+ import React from 'react';
30
+ import { useFileUpload } from 'squarefi-bff-api-module';
31
+
32
+ function FileUploadComponent({ userId }: { userId: string }) {
33
+ const { upload, uploading, progress, error, result } = useFileUpload({
34
+ userId,
35
+ onSuccess: (result) => {
36
+ console.log('File uploaded:', result.path);
37
+ console.log('Link:', result.signedUrl);
38
+ },
39
+ });
40
+
41
+ const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
42
+ const file = e.target.files?.[0];
43
+ if (file) {
44
+ await upload(file);
45
+ }
46
+ };
47
+
48
+ return (
49
+ <div>
50
+ <input
51
+ type="file"
52
+ onChange={handleFileChange}
53
+ disabled={uploading}
54
+ />
55
+
56
+ {uploading && <p>Uploading... {progress}%</p>}
57
+ {error && <p style={{ color: 'red' }}>Error: {error}</p>}
58
+ {result?.success && (
59
+ <p style={{ color: 'green' }}>
60
+ Successfully uploaded!
61
+ <a href={result.signedUrl} target="_blank" rel="noopener noreferrer">
62
+ Open file
63
+ </a>
64
+ </p>
65
+ )}
66
+ </div>
67
+ );
68
+ }
69
+
70
+ export default FileUploadComponent;
71
+ ```
72
+
73
+ ## 📋 File List with Hook
74
+
75
+ ```tsx
76
+ import React from 'react';
77
+ import { useUserFiles } from 'squarefi-bff-api-module';
78
+
79
+ function FileListComponent({ userId }: { userId: string }) {
80
+ const { files, loading, deleteOne, reload } = useUserFiles({
81
+ userId,
82
+ autoLoad: true,
83
+ autoGenerateUrls: true, // Automatically get URLs
84
+ });
85
+
86
+ if (loading) return <div>Loading...</div>;
87
+
88
+ return (
89
+ <div>
90
+ <button onClick={reload}>Refresh</button>
91
+
92
+ <ul>
93
+ {files.map((file) => (
94
+ <li key={file.id}>
95
+ <a href={file.signedUrl} target="_blank" rel="noopener noreferrer">
96
+ {file.name}
97
+ </a>
98
+ <span> ({new Date(file.created_at).toLocaleDateString()})</span>
99
+ <button onClick={() => deleteOne(file.name)}>Delete</button>
100
+ </li>
101
+ ))}
102
+ </ul>
103
+
104
+ {files.length === 0 && <p>No files</p>}
105
+ </div>
106
+ );
107
+ }
108
+
109
+ export default FileListComponent;
110
+ ```
111
+
112
+ ## 🎨 Complete Example: Component with Upload and List
113
+
114
+ ```tsx
115
+ import React, { useState } from 'react';
116
+ import { useFileUpload, useUserFiles, DEFAULT_BUCKET } from 'squarefi-bff-api-module';
117
+
118
+ interface FileManagerProps {
119
+ userId: string;
120
+ }
121
+
122
+ export const FileManager: React.FC<FileManagerProps> = ({ userId }) => {
123
+ const [selectedFile, setSelectedFile] = useState<File | null>(null);
124
+
125
+ // Upload hook
126
+ const { upload, uploading, progress, error: uploadError } = useFileUpload({
127
+ userId,
128
+ bucket: DEFAULT_BUCKET,
129
+ onSuccess: () => {
130
+ setSelectedFile(null);
131
+ reload(); // Refresh list after upload
132
+ },
133
+ });
134
+
135
+ // File list hook
136
+ const {
137
+ files,
138
+ loading,
139
+ error: listError,
140
+ reload,
141
+ deleteOne
142
+ } = useUserFiles({
143
+ userId,
144
+ autoLoad: true,
145
+ autoGenerateUrls: true,
146
+ });
147
+
148
+ const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
149
+ const file = e.target.files?.[0];
150
+ setSelectedFile(file || null);
151
+ };
152
+
153
+ const handleUpload = async () => {
154
+ if (selectedFile) {
155
+ await upload(selectedFile);
156
+ }
157
+ };
158
+
159
+ const handleDelete = async (fileName: string) => {
160
+ if (window.confirm(`Delete file ${fileName}?`)) {
161
+ await deleteOne(fileName);
162
+ }
163
+ };
164
+
165
+ return (
166
+ <div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
167
+ <h2>My Files</h2>
168
+
169
+ {/* Upload section */}
170
+ <div style={{ marginBottom: '30px', padding: '20px', border: '1px solid #ddd', borderRadius: '8px' }}>
171
+ <h3>Upload File</h3>
172
+ <input type="file" onChange={handleFileSelect} disabled={uploading} />
173
+
174
+ {selectedFile && (
175
+ <div style={{ marginTop: '10px' }}>
176
+ <p>Selected: {selectedFile.name} ({(selectedFile.size / 1024 / 1024).toFixed(2)} MB)</p>
177
+ <button onClick={handleUpload} disabled={uploading}>
178
+ {uploading ? `Uploading... ${progress}%` : 'Upload'}
179
+ </button>
180
+ </div>
181
+ )}
182
+
183
+ {uploadError && (
184
+ <p style={{ color: 'red', marginTop: '10px' }}>❌ {uploadError}</p>
185
+ )}
186
+ </div>
187
+
188
+ {/* File list section */}
189
+ <div>
190
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}>
191
+ <h3>Uploaded Files ({files.length})</h3>
192
+ <button onClick={reload} disabled={loading}>
193
+ 🔄 Refresh
194
+ </button>
195
+ </div>
196
+
197
+ {loading && <p>Loading list...</p>}
198
+
199
+ {listError && <p style={{ color: 'red' }}>❌ {listError}</p>}
200
+
201
+ {!loading && files.length === 0 && (
202
+ <p style={{ textAlign: 'center', color: '#888' }}>
203
+ You don't have any uploaded files yet
204
+ </p>
205
+ )}
206
+
207
+ {files.length > 0 && (
208
+ <table style={{ width: '100%', borderCollapse: 'collapse' }}>
209
+ <thead>
210
+ <tr style={{ borderBottom: '2px solid #ddd' }}>
211
+ <th style={{ textAlign: 'left', padding: '10px' }}>File</th>
212
+ <th style={{ textAlign: 'left', padding: '10px' }}>Date</th>
213
+ <th style={{ textAlign: 'right', padding: '10px' }}>Actions</th>
214
+ </tr>
215
+ </thead>
216
+ <tbody>
217
+ {files.map((file) => (
218
+ <tr key={file.id} style={{ borderBottom: '1px solid #eee' }}>
219
+ <td style={{ padding: '10px' }}>
220
+ <a
221
+ href={file.signedUrl}
222
+ target="_blank"
223
+ rel="noopener noreferrer"
224
+ style={{ color: '#007bff', textDecoration: 'none' }}
225
+ >
226
+ 📄 {file.name}
227
+ </a>
228
+ </td>
229
+ <td style={{ padding: '10px', color: '#666' }}>
230
+ {new Date(file.created_at).toLocaleDateString()}
231
+ </td>
232
+ <td style={{ padding: '10px', textAlign: 'right' }}>
233
+ <button
234
+ onClick={() => handleDelete(file.name)}
235
+ style={{
236
+ background: '#dc3545',
237
+ color: 'white',
238
+ border: 'none',
239
+ padding: '5px 10px',
240
+ borderRadius: '4px',
241
+ cursor: 'pointer'
242
+ }}
243
+ >
244
+ 🗑️ Delete
245
+ </button>
246
+ </td>
247
+ </tr>
248
+ ))}
249
+ </tbody>
250
+ </table>
251
+ )}
252
+ </div>
253
+ </div>
254
+ );
255
+ };
256
+ ```
257
+
258
+ ## 🖼️ Image Upload with Preview
259
+
260
+ ```tsx
261
+ import React, { useState } from 'react';
262
+ import { useFileUpload, IMAGES_BUCKET } from 'squarefi-bff-api-module';
263
+
264
+ export const ImageUploader: React.FC<{ userId: string }> = ({ userId }) => {
265
+ const [preview, setPreview] = useState<string | null>(null);
266
+
267
+ const { upload, uploading, error, result } = useFileUpload({
268
+ userId,
269
+ bucket: IMAGES_BUCKET,
270
+ });
271
+
272
+ const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
273
+ const file = e.target.files?.[0];
274
+ if (!file) return;
275
+
276
+ // Create preview
277
+ const reader = new FileReader();
278
+ reader.onloadend = () => {
279
+ setPreview(reader.result as string);
280
+ };
281
+ reader.readAsDataURL(file);
282
+
283
+ // Upload
284
+ await upload(file);
285
+ };
286
+
287
+ return (
288
+ <div>
289
+ <input
290
+ type="file"
291
+ accept="image/*"
292
+ onChange={handleFileChange}
293
+ disabled={uploading}
294
+ />
295
+
296
+ {preview && (
297
+ <div style={{ marginTop: '20px' }}>
298
+ <img
299
+ src={preview}
300
+ alt="Preview"
301
+ style={{ maxWidth: '300px', borderRadius: '8px' }}
302
+ />
303
+ </div>
304
+ )}
305
+
306
+ {uploading && <p>Uploading image...</p>}
307
+ {error && <p style={{ color: 'red' }}>{error}</p>}
308
+ {result?.success && (
309
+ <p style={{ color: 'green' }}>✅ Image uploaded!</p>
310
+ )}
311
+ </div>
312
+ );
313
+ };
314
+ ```
315
+
316
+ ## 🎯 Drag & Drop Upload
317
+
318
+ ```tsx
319
+ import React, { useState, useCallback } from 'react';
320
+ import { useFileUpload } from 'squarefi-bff-api-module';
321
+
322
+ export const DragDropUploader: React.FC<{ userId: string }> = ({ userId }) => {
323
+ const [isDragging, setIsDragging] = useState(false);
324
+ const { upload, uploading } = useFileUpload({ userId });
325
+
326
+ const handleDrop = useCallback(
327
+ async (e: React.DragEvent) => {
328
+ e.preventDefault();
329
+ setIsDragging(false);
330
+
331
+ const files = Array.from(e.dataTransfer.files);
332
+ for (const file of files) {
333
+ await upload(file);
334
+ }
335
+ },
336
+ [upload]
337
+ );
338
+
339
+ return (
340
+ <div
341
+ onDragOver={(e) => {
342
+ e.preventDefault();
343
+ setIsDragging(true);
344
+ }}
345
+ onDragLeave={() => setIsDragging(false)}
346
+ onDrop={handleDrop}
347
+ style={{
348
+ border: `3px dashed ${isDragging ? '#007bff' : '#ccc'}`,
349
+ borderRadius: '12px',
350
+ padding: '60px 40px',
351
+ textAlign: 'center',
352
+ backgroundColor: isDragging ? '#f0f8ff' : '#fafafa',
353
+ transition: 'all 0.3s ease',
354
+ cursor: 'pointer',
355
+ }}
356
+ >
357
+ {uploading ? (
358
+ <p>⏳ Uploading files...</p>
359
+ ) : (
360
+ <div>
361
+ <p style={{ fontSize: '48px', margin: '0 0 10px 0' }}>📁</p>
362
+ <p style={{ fontSize: '18px', margin: 0 }}>
363
+ Drag and drop files here
364
+ </p>
365
+ </div>
366
+ )}
367
+ </div>
368
+ );
369
+ };
370
+ ```
371
+
372
+ ## 🔧 Hook APIs
373
+
374
+ ### useFileUpload
375
+
376
+ ```typescript
377
+ const {
378
+ upload, // (file: File, fileName?: string) => Promise<UploadFileResult>
379
+ uploading, // boolean - is uploading
380
+ progress, // number - upload progress (0-100)
381
+ error, // string | null - error text
382
+ result, // UploadFileResult | null - upload result
383
+ reset, // () => void - reset state
384
+ } = useFileUpload({
385
+ userId: 'user-id', // required
386
+ bucket?: 'user-files', // optional
387
+ onSuccess?: (result) => {}, // success callback
388
+ onError?: (error) => {}, // error callback
389
+ });
390
+ ```
391
+
392
+ ### useUserFiles
393
+
394
+ ```typescript
395
+ const {
396
+ files, // FileItem[] - file list
397
+ loading, // boolean - is loading
398
+ error, // string | null - error text
399
+ reload, // () => Promise<void> - reload list
400
+ deleteOne, // (fileName: string) => Promise<boolean>
401
+ deleteMultiple, // (fileNames: string[]) => Promise<boolean>
402
+ getFileUrl, // (fileName: string) => Promise<string | null>
403
+ } = useUserFiles({
404
+ userId: 'user-id', // required
405
+ bucket?: 'user-files', // optional
406
+ autoLoad?: true, // auto-load on mount
407
+ autoGenerateUrls?: false, // auto-generate signed URLs
408
+ urlExpiresIn?: 3600, // URL expiration in seconds
409
+ });
410
+ ```
411
+
412
+ ## 💡 Tips
413
+
414
+ ### 1. Use different buckets for different file types
415
+
416
+ ```tsx
417
+ import { DOCUMENTS_BUCKET, IMAGES_BUCKET } from 'squarefi-bff-api-module';
418
+
419
+ // For documents
420
+ const { upload: uploadDoc } = useFileUpload({
421
+ userId,
422
+ bucket: DOCUMENTS_BUCKET
423
+ });
424
+
425
+ // For images
426
+ const { upload: uploadImage } = useFileUpload({
427
+ userId,
428
+ bucket: IMAGES_BUCKET
429
+ });
430
+ ```
431
+
432
+ ### 2. Validate files before upload
433
+
434
+ ```tsx
435
+ const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
436
+ const file = e.target.files?.[0];
437
+ if (!file) return;
438
+
439
+ // Check size (max 10MB)
440
+ if (file.size > 10 * 1024 * 1024) {
441
+ alert('File too large. Maximum 10MB');
442
+ return;
443
+ }
444
+
445
+ // Check type
446
+ const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
447
+ if (!allowedTypes.includes(file.type)) {
448
+ alert('Unsupported file type');
449
+ return;
450
+ }
451
+
452
+ await upload(file);
453
+ };
454
+ ```
455
+
456
+ ### 3. Add unique prefix to file name
457
+
458
+ ```tsx
459
+ // Avoid name conflicts
460
+ const timestamp = Date.now();
461
+ const randomId = Math.random().toString(36).substring(7);
462
+ const uniqueFileName = `${timestamp}-${randomId}-${file.name}`;
463
+
464
+ await upload(file, uniqueFileName);
465
+ ```
466
+
467
+ ### 4. Error handling
468
+
469
+ ```tsx
470
+ const { upload } = useFileUpload({
471
+ userId,
472
+ onSuccess: (result) => {
473
+ toast.success('File uploaded!');
474
+ // Save result.path in your DB
475
+ },
476
+ onError: (error) => {
477
+ toast.error(`Error: ${error}`);
478
+ },
479
+ });
480
+ ```
481
+
482
+ ## 🔒 Security
483
+
484
+ All files are automatically protected:
485
+
486
+ - ✅ User can only upload files to their own folder
487
+ - ✅ User can only see their own files
488
+ - ✅ Superadmins have access to all files
489
+ - ✅ Use `signedUrl` for temporary secure access
490
+ - ✅ Don't use `publicUrl` for confidential data
491
+
492
+ ## 📚 Additional Resources
493
+
494
+ - [Full Documentation](./STORAGE_MODULE.md)
495
+ - [Examples for Different Frameworks](./STORAGE_EXAMPLES.md)
496
+ - [SQL Setup Script](../scripts/supabase-storage-setup.sql)
497
+
498
+ ## ❓ FAQ
499
+
500
+ **Q: How to get current userId?**
501
+ A: Use your authentication system (Supabase Auth, JWT token, etc.)
502
+
503
+ ```tsx
504
+ import { supabaseClient } from 'squarefi-bff-api-module';
505
+
506
+ const userId = supabaseClient?.auth.user()?.id;
507
+ ```
508
+
509
+ **Q: Files not uploading, what to do?**
510
+ A: Check:
511
+ 1. SQL script executed in Supabase
512
+ 2. Environment variables are set
513
+ 3. User is authenticated in Supabase
514
+
515
+ **Q: How to change maximum file size?**
516
+ A: In Supabase Dashboard → Settings → Storage → File Size Limit
517
+
518
+ **Q: Can I use without React?**
519
+ A: Yes! Use functions directly: `uploadFile()`, `deleteFile()`, etc.
520
+
521
+ ```typescript
522
+ import { uploadFile } from 'squarefi-bff-api-module';
523
+
524
+ const result = await uploadFile({
525
+ file: myFile,
526
+ fileName: 'test.pdf',
527
+ userId: 'user-123',
528
+ });
529
+ ```