squarefi-bff-api-module 1.30.9 → 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.
- package/CHANGELOG.md +296 -1
- package/FIXED_RLS_ERROR.md +146 -0
- package/QUICK_TEST.md +127 -0
- package/README.md +87 -10
- package/STORAGE_MODULE_SUMMARY.md +228 -0
- package/TEST_INSTRUCTIONS.md +122 -0
- package/dist/api/types/types.d.ts +2 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useFileUpload.d.ts +18 -3
- package/dist/hooks/useFileUpload.js +19 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/utils/fileStorage.d.ts +8 -4
- package/dist/utils/fileStorage.js +8 -4
- package/docs/AUTH_TOKEN_USAGE.md +290 -0
- package/docs/BACKEND_SERVICE_URL.md +334 -0
- package/docs/FRONTEND_STORAGE_GUIDE.md +529 -0
- package/docs/READY_TO_USE_COMPONENT.tsx +395 -0
- package/docs/STORAGE_MODULE.md +490 -0
- package/docs/STORAGE_QUICK_START.md +76 -0
- package/package.json +1 -1
- package/scripts/supabase-storage-setup.sql +223 -0
- package/src/api/types/types.ts +2 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useFileUpload.ts +129 -0
- package/src/index.ts +1 -0
- package/src/utils/fileStorage.ts +367 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ready-to-use File Manager component
|
|
3
|
+
*
|
|
4
|
+
* Copy this file to your project and use it!
|
|
5
|
+
*
|
|
6
|
+
* Installation:
|
|
7
|
+
* npm install squarefi-bff-api-module
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* import { FileManager } from './FileManager';
|
|
11
|
+
*
|
|
12
|
+
* <FileManager userId="user-123" />
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import React, { useState } from 'react';
|
|
16
|
+
import {
|
|
17
|
+
useFileUpload,
|
|
18
|
+
useUserFiles,
|
|
19
|
+
DEFAULT_BUCKET,
|
|
20
|
+
} from 'squarefi-bff-api-module';
|
|
21
|
+
|
|
22
|
+
interface FileManagerProps {
|
|
23
|
+
userId: string;
|
|
24
|
+
bucket?: string;
|
|
25
|
+
maxFileSize?: number; // in bytes
|
|
26
|
+
allowedTypes?: string[];
|
|
27
|
+
onFileUpload?: (path: string) => void;
|
|
28
|
+
onFileDelete?: (fileName: string) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Universal component for managing user files
|
|
33
|
+
*/
|
|
34
|
+
export const FileManager: React.FC<FileManagerProps> = ({
|
|
35
|
+
userId,
|
|
36
|
+
bucket = DEFAULT_BUCKET,
|
|
37
|
+
maxFileSize = 10 * 1024 * 1024, // 10MB by default
|
|
38
|
+
allowedTypes = ['*/*'], // all types by default
|
|
39
|
+
onFileUpload,
|
|
40
|
+
onFileDelete,
|
|
41
|
+
}) => {
|
|
42
|
+
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
|
43
|
+
const [validationError, setValidationError] = useState<string | null>(null);
|
|
44
|
+
|
|
45
|
+
// File upload hook
|
|
46
|
+
const { upload, uploading, progress, error: uploadError } = useFileUpload({
|
|
47
|
+
userId,
|
|
48
|
+
bucket,
|
|
49
|
+
onSuccess: (result) => {
|
|
50
|
+
setSelectedFile(null);
|
|
51
|
+
setValidationError(null);
|
|
52
|
+
if (result.path) {
|
|
53
|
+
onFileUpload?.(result.path);
|
|
54
|
+
}
|
|
55
|
+
reload(); // Refresh list after upload
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// File list hook
|
|
60
|
+
const {
|
|
61
|
+
files,
|
|
62
|
+
loading,
|
|
63
|
+
error: listError,
|
|
64
|
+
reload,
|
|
65
|
+
deleteOne,
|
|
66
|
+
} = useUserFiles({
|
|
67
|
+
userId,
|
|
68
|
+
bucket,
|
|
69
|
+
autoLoad: true,
|
|
70
|
+
autoGenerateUrls: true,
|
|
71
|
+
urlExpiresIn: 3600,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// File validation
|
|
75
|
+
const validateFile = (file: File): string | null => {
|
|
76
|
+
// Size check
|
|
77
|
+
if (file.size > maxFileSize) {
|
|
78
|
+
return `File too large. Maximum ${(maxFileSize / 1024 / 1024).toFixed(0)}MB`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Type check
|
|
82
|
+
if (!allowedTypes.includes('*/*')) {
|
|
83
|
+
const isAllowed = allowedTypes.some((type) => {
|
|
84
|
+
if (type.endsWith('/*')) {
|
|
85
|
+
// Example: image/*
|
|
86
|
+
const prefix = type.split('/')[0];
|
|
87
|
+
return file.type.startsWith(prefix + '/');
|
|
88
|
+
}
|
|
89
|
+
return file.type === type;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (!isAllowed) {
|
|
93
|
+
return `Unsupported file type. Allowed: ${allowedTypes.join(', ')}`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return null;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
101
|
+
const file = e.target.files?.[0];
|
|
102
|
+
if (!file) return;
|
|
103
|
+
|
|
104
|
+
const error = validateFile(file);
|
|
105
|
+
if (error) {
|
|
106
|
+
setValidationError(error);
|
|
107
|
+
setSelectedFile(null);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
setValidationError(null);
|
|
112
|
+
setSelectedFile(file);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const handleUpload = async () => {
|
|
116
|
+
if (!selectedFile) return;
|
|
117
|
+
|
|
118
|
+
const timestamp = Date.now();
|
|
119
|
+
const uniqueFileName = `${timestamp}-${selectedFile.name}`;
|
|
120
|
+
await upload(selectedFile, uniqueFileName);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const handleDelete = async (fileName: string) => {
|
|
124
|
+
const confirmed = window.confirm(`Are you sure you want to delete "${fileName}"?`);
|
|
125
|
+
if (!confirmed) return;
|
|
126
|
+
|
|
127
|
+
const success = await deleteOne(fileName);
|
|
128
|
+
if (success) {
|
|
129
|
+
onFileDelete?.(fileName);
|
|
130
|
+
} else {
|
|
131
|
+
alert('Failed to delete file');
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const formatFileSize = (bytes: number): string => {
|
|
136
|
+
if (bytes < 1024) return bytes + ' B';
|
|
137
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
138
|
+
return (bytes / 1024 / 1024).toFixed(1) + ' MB';
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const formatDate = (dateString: string): string => {
|
|
142
|
+
const date = new Date(dateString);
|
|
143
|
+
return date.toLocaleDateString('en-US', {
|
|
144
|
+
year: 'numeric',
|
|
145
|
+
month: 'short',
|
|
146
|
+
day: 'numeric',
|
|
147
|
+
hour: '2-digit',
|
|
148
|
+
minute: '2-digit',
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div style={styles.container}>
|
|
154
|
+
<h2 style={styles.title}>File Management</h2>
|
|
155
|
+
|
|
156
|
+
{/* Upload section */}
|
|
157
|
+
<div style={styles.uploadSection}>
|
|
158
|
+
<h3 style={styles.sectionTitle}>📤 Upload File</h3>
|
|
159
|
+
|
|
160
|
+
<input
|
|
161
|
+
type="file"
|
|
162
|
+
onChange={handleFileSelect}
|
|
163
|
+
disabled={uploading}
|
|
164
|
+
style={styles.fileInput}
|
|
165
|
+
accept={allowedTypes.join(',')}
|
|
166
|
+
/>
|
|
167
|
+
|
|
168
|
+
{selectedFile && !validationError && (
|
|
169
|
+
<div style={styles.selectedFile}>
|
|
170
|
+
<p style={styles.fileName}>
|
|
171
|
+
📄 {selectedFile.name} ({formatFileSize(selectedFile.size)})
|
|
172
|
+
</p>
|
|
173
|
+
<button
|
|
174
|
+
onClick={handleUpload}
|
|
175
|
+
disabled={uploading}
|
|
176
|
+
style={{
|
|
177
|
+
...styles.button,
|
|
178
|
+
...styles.uploadButton,
|
|
179
|
+
...(uploading ? styles.buttonDisabled : {}),
|
|
180
|
+
}}
|
|
181
|
+
>
|
|
182
|
+
{uploading ? `⏳ Uploading... ${progress}%` : '✅ Upload'}
|
|
183
|
+
</button>
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
{validationError && (
|
|
188
|
+
<p style={styles.errorText}>❌ {validationError}</p>
|
|
189
|
+
)}
|
|
190
|
+
|
|
191
|
+
{uploadError && (
|
|
192
|
+
<p style={styles.errorText}>❌ Upload error: {uploadError}</p>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
{/* File list section */}
|
|
197
|
+
<div style={styles.listSection}>
|
|
198
|
+
<div style={styles.listHeader}>
|
|
199
|
+
<h3 style={styles.sectionTitle}>
|
|
200
|
+
📁 Your Files ({files.length})
|
|
201
|
+
</h3>
|
|
202
|
+
<button
|
|
203
|
+
onClick={reload}
|
|
204
|
+
disabled={loading}
|
|
205
|
+
style={{
|
|
206
|
+
...styles.button,
|
|
207
|
+
...styles.refreshButton,
|
|
208
|
+
...(loading ? styles.buttonDisabled : {}),
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
🔄 Refresh
|
|
212
|
+
</button>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
{loading && <p style={styles.loadingText}>Loading file list...</p>}
|
|
216
|
+
|
|
217
|
+
{listError && (
|
|
218
|
+
<p style={styles.errorText}>❌ List loading error: {listError}</p>
|
|
219
|
+
)}
|
|
220
|
+
|
|
221
|
+
{!loading && files.length === 0 && (
|
|
222
|
+
<p style={styles.emptyText}>You don't have any uploaded files yet</p>
|
|
223
|
+
)}
|
|
224
|
+
|
|
225
|
+
{files.length > 0 && (
|
|
226
|
+
<div style={styles.filesList}>
|
|
227
|
+
{files.map((file) => (
|
|
228
|
+
<div key={file.id} style={styles.fileItem}>
|
|
229
|
+
<div style={styles.fileInfo}>
|
|
230
|
+
<a
|
|
231
|
+
href={file.signedUrl}
|
|
232
|
+
target="_blank"
|
|
233
|
+
rel="noopener noreferrer"
|
|
234
|
+
style={styles.fileLink}
|
|
235
|
+
>
|
|
236
|
+
📄 {file.name}
|
|
237
|
+
</a>
|
|
238
|
+
<p style={styles.fileDate}>
|
|
239
|
+
{formatDate(file.created_at)}
|
|
240
|
+
</p>
|
|
241
|
+
</div>
|
|
242
|
+
<button
|
|
243
|
+
onClick={() => handleDelete(file.name)}
|
|
244
|
+
style={{
|
|
245
|
+
...styles.button,
|
|
246
|
+
...styles.deleteButton,
|
|
247
|
+
}}
|
|
248
|
+
>
|
|
249
|
+
🗑️ Delete
|
|
250
|
+
</button>
|
|
251
|
+
</div>
|
|
252
|
+
))}
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// Стили компонента
|
|
261
|
+
const styles: { [key: string]: React.CSSProperties } = {
|
|
262
|
+
container: {
|
|
263
|
+
padding: '20px',
|
|
264
|
+
maxWidth: '900px',
|
|
265
|
+
margin: '0 auto',
|
|
266
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
267
|
+
},
|
|
268
|
+
title: {
|
|
269
|
+
fontSize: '28px',
|
|
270
|
+
fontWeight: 'bold',
|
|
271
|
+
marginBottom: '30px',
|
|
272
|
+
color: '#333',
|
|
273
|
+
},
|
|
274
|
+
uploadSection: {
|
|
275
|
+
marginBottom: '40px',
|
|
276
|
+
padding: '25px',
|
|
277
|
+
border: '2px solid #e0e0e0',
|
|
278
|
+
borderRadius: '12px',
|
|
279
|
+
backgroundColor: '#fafafa',
|
|
280
|
+
},
|
|
281
|
+
listSection: {
|
|
282
|
+
padding: '25px',
|
|
283
|
+
border: '2px solid #e0e0e0',
|
|
284
|
+
borderRadius: '12px',
|
|
285
|
+
backgroundColor: '#ffffff',
|
|
286
|
+
},
|
|
287
|
+
sectionTitle: {
|
|
288
|
+
fontSize: '20px',
|
|
289
|
+
fontWeight: '600',
|
|
290
|
+
marginBottom: '15px',
|
|
291
|
+
color: '#555',
|
|
292
|
+
},
|
|
293
|
+
fileInput: {
|
|
294
|
+
padding: '10px',
|
|
295
|
+
fontSize: '16px',
|
|
296
|
+
border: '2px solid #ddd',
|
|
297
|
+
borderRadius: '8px',
|
|
298
|
+
width: '100%',
|
|
299
|
+
cursor: 'pointer',
|
|
300
|
+
},
|
|
301
|
+
selectedFile: {
|
|
302
|
+
marginTop: '15px',
|
|
303
|
+
padding: '15px',
|
|
304
|
+
backgroundColor: '#e8f5e9',
|
|
305
|
+
borderRadius: '8px',
|
|
306
|
+
},
|
|
307
|
+
fileName: {
|
|
308
|
+
margin: '0 0 10px 0',
|
|
309
|
+
fontSize: '16px',
|
|
310
|
+
color: '#333',
|
|
311
|
+
},
|
|
312
|
+
button: {
|
|
313
|
+
padding: '10px 20px',
|
|
314
|
+
fontSize: '16px',
|
|
315
|
+
border: 'none',
|
|
316
|
+
borderRadius: '8px',
|
|
317
|
+
cursor: 'pointer',
|
|
318
|
+
transition: 'all 0.2s ease',
|
|
319
|
+
fontWeight: '500',
|
|
320
|
+
},
|
|
321
|
+
uploadButton: {
|
|
322
|
+
backgroundColor: '#4caf50',
|
|
323
|
+
color: 'white',
|
|
324
|
+
},
|
|
325
|
+
refreshButton: {
|
|
326
|
+
backgroundColor: '#2196f3',
|
|
327
|
+
color: 'white',
|
|
328
|
+
fontSize: '14px',
|
|
329
|
+
padding: '8px 16px',
|
|
330
|
+
},
|
|
331
|
+
deleteButton: {
|
|
332
|
+
backgroundColor: '#f44336',
|
|
333
|
+
color: 'white',
|
|
334
|
+
fontSize: '14px',
|
|
335
|
+
padding: '6px 12px',
|
|
336
|
+
},
|
|
337
|
+
buttonDisabled: {
|
|
338
|
+
opacity: 0.6,
|
|
339
|
+
cursor: 'not-allowed',
|
|
340
|
+
},
|
|
341
|
+
listHeader: {
|
|
342
|
+
display: 'flex',
|
|
343
|
+
justifyContent: 'space-between',
|
|
344
|
+
alignItems: 'center',
|
|
345
|
+
marginBottom: '20px',
|
|
346
|
+
},
|
|
347
|
+
filesList: {
|
|
348
|
+
display: 'flex',
|
|
349
|
+
flexDirection: 'column',
|
|
350
|
+
gap: '10px',
|
|
351
|
+
},
|
|
352
|
+
fileItem: {
|
|
353
|
+
display: 'flex',
|
|
354
|
+
justifyContent: 'space-between',
|
|
355
|
+
alignItems: 'center',
|
|
356
|
+
padding: '15px',
|
|
357
|
+
border: '1px solid #e0e0e0',
|
|
358
|
+
borderRadius: '8px',
|
|
359
|
+
backgroundColor: '#fafafa',
|
|
360
|
+
transition: 'background-color 0.2s ease',
|
|
361
|
+
},
|
|
362
|
+
fileInfo: {
|
|
363
|
+
flex: 1,
|
|
364
|
+
},
|
|
365
|
+
fileLink: {
|
|
366
|
+
color: '#1976d2',
|
|
367
|
+
textDecoration: 'none',
|
|
368
|
+
fontSize: '16px',
|
|
369
|
+
fontWeight: '500',
|
|
370
|
+
},
|
|
371
|
+
fileDate: {
|
|
372
|
+
margin: '5px 0 0 0',
|
|
373
|
+
fontSize: '13px',
|
|
374
|
+
color: '#888',
|
|
375
|
+
},
|
|
376
|
+
errorText: {
|
|
377
|
+
color: '#f44336',
|
|
378
|
+
marginTop: '10px',
|
|
379
|
+
fontSize: '14px',
|
|
380
|
+
},
|
|
381
|
+
loadingText: {
|
|
382
|
+
textAlign: 'center',
|
|
383
|
+
color: '#888',
|
|
384
|
+
fontSize: '16px',
|
|
385
|
+
},
|
|
386
|
+
emptyText: {
|
|
387
|
+
textAlign: 'center',
|
|
388
|
+
color: '#aaa',
|
|
389
|
+
fontSize: '16px',
|
|
390
|
+
padding: '30px',
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
export default FileManager;
|
|
395
|
+
|